﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;

namespace Microsoft.DotNet.Cli.Utils;

/// <summary>
/// A Command that is capable of running in the current process.
/// </summary>
public class BuiltInCommand : ICommand
{
    private readonly IEnumerable<string> _commandArgs;
    private readonly Func<string[], int> _builtInCommand;
    private readonly IBuiltInCommandEnvironment _environment;
    private readonly StreamForwarder _stdOut;
    private readonly StreamForwarder _stdErr;
    private string? _workingDirectory;

    public string CommandName { get; }
    public string CommandArgs => string.Join(" ", _commandArgs);

    public BuiltInCommand(string commandName, IEnumerable<string> commandArgs, Func<string[], int> builtInCommand)
        : this(commandName, commandArgs, builtInCommand, new BuiltInCommandEnvironment())
    {
    }

    internal BuiltInCommand(string commandName, IEnumerable<string> commandArgs, Func<string[], int> builtInCommand, IBuiltInCommandEnvironment environment)
    {
        CommandName = commandName;
        _commandArgs = commandArgs;
        _builtInCommand = builtInCommand;
        _environment = environment;

        _stdOut = new StreamForwarder();
        _stdErr = new StreamForwarder();
    }

    public CommandResult Execute()
    {
        TextWriter originalConsoleOut = _environment.GetConsoleOut();
        TextWriter originalConsoleError = _environment.GetConsoleError();
        string originalWorkingDirectory = _environment.GetWorkingDirectory();

        try
        {
            // redirecting the standard out and error so we can forward
            // the output to the caller
            using (BlockingMemoryStream outStream = new())
            using (BlockingMemoryStream errorStream = new())
            {
                _environment.SetConsoleOut(new StreamWriter(outStream) { AutoFlush = true });
                _environment.SetConsoleError(new StreamWriter(errorStream) { AutoFlush = true });

                // Reset the Reporters to the new Console Out and Error.
                Reporter.Reset();

                if (_workingDirectory is not null && _workingDirectory.Length != 0)
                {
                    _environment.SetWorkingDirectory(_workingDirectory);
                }

                var taskOut = _stdOut.BeginRead(new StreamReader(outStream));
                var taskErr = _stdErr.BeginRead(new StreamReader(errorStream));

                int exitCode = _builtInCommand([.. _commandArgs]);

                outStream.DoneWriting();
                errorStream.DoneWriting();

                Task.WaitAll(taskOut, taskErr);

                // fake out a ProcessStartInfo using the Muxer command name, since this is a built-in command
                ProcessStartInfo startInfo = new(new Muxer().MuxerPath, $"{CommandName} {CommandArgs}");
                return new CommandResult(startInfo, exitCode, null, null);
            }
        }
        finally
        {
            _environment.SetConsoleOut(originalConsoleOut);
            _environment.SetConsoleError(originalConsoleError);
            _environment.SetWorkingDirectory(originalWorkingDirectory);

            Reporter.Reset();
        }
    }

    public ICommand OnOutputLine(Action<string> handler)
    {
        if (handler == null)
        {
            throw new ArgumentNullException(nameof(handler));
        }

        _stdOut.ForwardTo(writeLine: handler);

        return this;
    }

    public ICommand OnErrorLine(Action<string> handler)
    {
        if (handler == null)
        {
            throw new ArgumentNullException(nameof(handler));
        }

        _stdErr.ForwardTo(writeLine: handler);

        return this;
    }

    public ICommand WorkingDirectory(string workingDirectory)
    {
        _workingDirectory = workingDirectory;

        return this;
    }

    private class BuiltInCommandEnvironment : IBuiltInCommandEnvironment
    {
        public TextWriter GetConsoleOut()
        {
            return Console.Out;
        }

        public void SetConsoleOut(TextWriter newOut)
        {
            Console.SetOut(newOut);
        }

        public TextWriter GetConsoleError()
        {
            return Console.Error;
        }

        public void SetConsoleError(TextWriter newError)
        {
            Console.SetError(newError);
        }

        public string GetWorkingDirectory()
        {
            return Directory.GetCurrentDirectory();
        }

        public void SetWorkingDirectory(string path)
        {
            Directory.SetCurrentDirectory(path);
        }
    }

    public ICommand CaptureStdErr()
    {
        _stdErr.Capture();

        return this;
    }

    public ICommand CaptureStdOut()
    {
        _stdOut.Capture();

        return this;
    }

    public ICommand EnvironmentVariable(string name, string? value)
    {
        throw new NotImplementedException();
    }

    public ICommand ForwardStdErr(TextWriter? to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
    {
        throw new NotImplementedException();
    }

    public ICommand ForwardStdOut(TextWriter? to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true)
    {
        throw new NotImplementedException();
    }
    public ICommand SetCommandArgs(string commandArgs)
    {
        throw new NotImplementedException();
    }

    public ICommand StandardOutputEncoding(Encoding encoding)
    {
        throw new NotImplementedException();
    }
}
